繼續往下看到 #init_receive
方法的部分,不過出現了一些比較少見的程式碼。
# 受信コマンドの初期化
def init_receive(cmd)
cmd.each do |c|
n = c[0].id2name+"_r"
@method_list << n.intern
ret = <<-EOF
def #{n}(data)
# p data
#{gen_receve_cmd(c[1],c[0])}
end
EOF
puts ret if OUTPUT_EVAL
@klass.class_eval(ret)
end
end
首先是 #id2name
這個方法,如果稍微查一下文件的話,會發現其實就是 #to_s
方法,這是因為想要產生一個叫做 xxx_r
的字串,例如 register_r
這樣的名稱,而 #intern
這個方法也是一樣的,只是再將她轉換回 Symbol 而以,跟 #to_sym
的意思是差不多的。
在 CRuby 實作中,我們熟悉的 Symbol 其實是用
intern
命名的,所以如果嘗試在原始碼裡面找 Symbol 反而會找不到太多線索。這邊 Unlight 沒有用大家熟悉的#to_s
和#to_sym
的原因不知道是什麼,也許是有什麼特殊考量。
接下來的 ret
字串比較有趣,Unlight 將一段 Ruby 程式碼用字串存起來,並且帶入了生成的指令名稱(register_r
),並且透過前面指令定義的資訊呼叫 #gen_receive_cmd
這個方法。
最後在對初始化指令的對象(AuthServer
物件)執行 .class_eval
方法,利用 Ruby DSL 的特性將這個字串轉變成 Ruby 程式碼執行。
以 register
指令生成 Ruby 指令為例子做 .class_eval
,基本上跟下面這段程式碼的效果是一樣的。
class AuthServer
def register_r
# ... (gen_receive_cmd)
end
end
在 Ruby on Rails 裡面也會應用這樣的技巧動態生成某些行為(Ex. Model 相關物件,像是 User_Relation
這種物件)不過在這邊 Unlight 是透過這樣的方式減少重複撰寫定義指令解析處理的行為,每個指令解析的行為跟邏輯是固定重複撰寫並沒有意義,而這種方式也可以避免動態的判斷跟處理,不用製作很多物件來解析跟判斷,反而能加快運行速度。
在其他語言上類似 C 語言的 Macro (巨集)或者 Golang 的 Generator 的使用方式,雖然原理跟定義上有些差異但是達到的效果是類似的。
了解 Unlight 如何動態生成指令之後,還需要分析 Unlight 為什麼要使用 #gen_receive_cmd
來動態產生指令的內容。
# 受信コマンドの中身の文字列を生成する
def gen_receve_cmd(val, name)
ret = ''
pos = 0
s = ''
q = ''
if val
val.each do|i|
c = @cmd_val.new(i[0], i[1],i[2])
if c.size == 0
ret << " #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
# ret << "p data[#{pos},2]\n"
pos += 2
# ret << "p #{c.name}_len\n"
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
s << "#{c.name}_len+"
else
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"
pos += c.size
end
q << c.name+","
end
end
ret << " #{name}(#{q.chop!})"
ret
end
這段程式碼看似複雜,不過實際上我們以 register
指令作為例子,在 register_r
這個被動態定義的方法來看,我們會收到一個 data
的指令的內容,但是註冊指令本身需要五個參數(請參考上一篇)才能夠完成,但是這個 data
的指令內容是一段 Byte Array 要怎麼拆解才能得到對的數值呢?
在 #gen_receive_cmd
裡面我們可以看到一些線索:
val
的解析,也就是參數我們先看指令執行的部分,也就是最後兩行:
# ...
ret << " #{name}(#{q.chop!})"
ret
# ...
動作上來說很簡單,就是直接插入一個 register(name, ...)
這段程式碼,用來呼叫另一個 Ruby 方法。
因此前面生成接收方法會使用
register_r
是為了區分處理資料的部分跟實際上定義的邏輯register
。
接下來就是指令解析的部分
# ...
val.each do|i|
c = @cmd_val.new(i[0], i[1],i[2])
if c.size == 0
ret << " #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
# ret << "p data[#{pos},2]\n"
pos += 2
# ret << "p #{c.name}_len\n"
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
s << "#{c.name}_len+"
else
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"
pos += c.size
end
q << c.name+","
end
# ...
看起來似乎很複雜,實際上就是區分為「長度固定」跟「長度不固定」兩種情況。
前面在定義指令的時候應該有發現字串的大小(size
)設定值是 0
明顯不合理,但是字串的長度肯定是不固定的,因此在 Unlight 終會定義大小為 0
來表示需要另外處理才能夠取得資料。
針對長度不固定的資料是基於下面這段處理的:
# ...
ret << " #{c.name}_len = data[#{s unless s==""}#{pos},2].unpack('n*')[0]\n"
# ret << "p data[#{pos},2]\n"
pos += 2
# ret << "p #{c.name}_len\n"
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.name}_len]#{type_rec_res(c.type)}\n"
s << "#{c.name}_len+"
# ...
不過這樣有點難以理解,我們以 register
指令的第一個參數 name
來當做例子,實際上會寫成這樣
name_len = data[pos,2].unpack('n*')[0]
pos += 2
name = data[pos,name_len]
如此一來就比較好懂了,在 Unlight 裡面定義長度不固定的類型的前 2 Bytes 就是表示這段資料的長度,因此要先抓出來獲取這段資料的長度,那麼從第三個 Byte 開始算到到指定的長度就是資料的本體。
至於其他類型的資料如果長度是固定的,就很容易處理:
ret << " #{c.name} = data[#{s unless s==""}#{pos},#{c.size}]#{type_rec_res(c.type)}\n"
因為只需要基於目前的指標位置(pos
)開始讀取當初設定的長度,然後依照類型 (#type_rec_res
)查詢計裡方是再做解析就能正確地處理資料。
以 :char
類型為例子,查詢的到的處理方式是用 .unpack('c')[0]
組裡,因此就會是 data[pos,1].unpack('c')[0]
這樣的程式碼被執行。
到此為止,我們已經知道應該要如何產生指令封包給伺服器讀取跟解析。但是伺服器呼叫方法後是怎麼動作的,這就要來討論 Unlight 的 Controller 了。當然,這不會是我們平常討論的 MVC 的那個 Controller 而是在 Unlight 裡面表示指令動作的行為集合。
我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。